package com;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Random;
import java.util.Vector;

import org.apache.xmlrpc.WebServer;

import de.dfki.util.xmlrpc.XmlRpc;
import de.dfki.util.xmlrpc.XmlRpc.Type;
import de.dfki.util.xmlrpc.client.XmlRpcClient;
import de.dfki.util.xmlrpc.server.XmlRpcHandlerFactory;

public class Peer {
	private String[] colors = {"aquamarine", "azure", "beige", "bisque", "black", "blanchedalmond", "blue", "blueviolet",
			"brown", "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", "cornsilk", "crimson",
			"cyan", "darkgoldenrod", "darkgreen", "darkkhaki", "darkolivegreen", "darkorange", "darkorchid", "darksalmon",
			"darkseagreen", "darkslateblue", "darkslategray", "darkturquoise", "darkviolet", "deeppink", "deepskyblue", 
			"dimgray", "dodgerblue", "firebrick", "forestgreen", "gainsboro", "ghostwhite", "gold", "goldenrod", 
			"green", "greenyellow", "honeydew", "hotpink", "indianred", "indigo", "invis", "ivory", "khaki", "lavender", 
			"lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan", "lightcyan1", "lightcyan2", "lightcyan3", "lightcyan4", "lightgoldenrod", "lightgoldenrod1", "lightgoldenrod2", "lightgoldenrod3", "lightgoldenrod4", "lightgoldenrodyellow", "lightgray", "lightgrey", "lightpink", "lightpink1", "lightpink2", "lightpink3", "lightpink4", "lightsalmon", "lightsalmon1", "lightsalmon2", "lightsalmon3", "lightsalmon4", "lightseagreen", "lightskyblue", "lightskyblue1", "lightskyblue2", "lightskyblue3", "lightskyblue4", "lightslateblue", "lightslategray", "lightslategrey", "lightsteelblue", "lightsteelblue1", "lightsteelblue2", "lightsteelblue3", "lightsteelblue4", "lightyellow", "lightyellow1", "lightyellow2", "lightyellow3", "lightyellow4", "limegreen", "linen", "magenta", "magenta1", "magenta2", "magenta3", "magenta4", "maroon", "maroon1", "maroon2", "maroon3", "maroon4", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumorchid1", "mediumorchid2", "mediumorchid3", "mediumorchid4", "mediumpurple", "mediumpurple1", "mediumpurple2", "mediumpurple3", "mediumpurple4", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "mistyrose1", "mistyrose2", "mistyrose3", "mistyrose4", "moccasin", "navajowhite", "navajowhite1", "navajowhite2", "navajowhite3", "navajowhite4", "navy", "navyblue", "none", "oldlace", "olivedrab", "olivedrab1", "olivedrab2", "olivedrab3", "olivedrab4", "orange", "orange1", "orange2", "orange3", "orange4", "orangered", "orangered1", "orangered2", "orangered3", "orangered4", "orchid", "orchid1", "orchid2", "orchid3", "orchid4", "palegoldenrod", "palegreen", "palegreen1", "palegreen2", "palegreen3", "palegreen4", "paleturquoise", "paleturquoise1", "paleturquoise2", "paleturquoise3", "paleturquoise4", "palevioletred", "palevioletred1", "palevioletred2", "palevioletred3", "palevioletred4", "papayawhip", "peachpuff", "peachpuff1", "peachpuff2", "peachpuff3", "peachpuff4", "peru", "pink", "pink1", "pink2", "pink3", "pink4", "plum", "plum1", "plum2", "plum3", "plum4", "powderblue", "purple", "purple1", "purple2", "purple3", "purple4", "red", "red1", "red2", "red3", "red4", "rosybrown", "rosybrown1", "rosybrown2", "rosybrown3", "rosybrown4", "royalblue", "royalblue1", "royalblue2", "royalblue3", "royalblue4", "saddlebrown", "salmon", "salmon1", "salmon2", "salmon3", "salmon4", "sandybrown", "seagreen", "seagreen1", "seagreen2", "seagreen3", "seagreen4", "seashell", "seashell1", "seashell2", "seashell3", "seashell4", "sienna", "sienna1", "sienna2", "sienna3", "sienna4", "skyblue", "skyblue1", "skyblue2", "skyblue3", "skyblue4", "slateblue", "slateblue1", "slateblue2", "slateblue3", "slateblue4", "slategray", "slategray1", "slategray2", "slategray3", "slategray4", "slategrey", "snow", "snow1", "snow2", "snow3", "snow4", "springgreen", "springgreen1", "springgreen2", "springgreen3", "springgreen4", "steelblue", "steelblue1", "steelblue2", "steelblue3", "steelblue4", "tan", "tan1", "tan2", "tan3", "tan4", "thistle", "thistle1", "thistle2", "thistle3", "thistle4", "tomato", "tomato1", "tomato2", "tomato3", "tomato4", "transparent", "turquoise", "turquoise1", "turquoise2", "turquoise3", "turquoise4", "violet", "violetred", "violetred1", "violetred2", "violetred3", "violetred4", "wheat", "wheat1", "wheat2", "wheat3", "wheat4", "white", "whitesmoke", "yellow", "yellow1", "yellow2", "yellow3", "yellow4", "yellowgreen" };
	private int color_i = 0;
	public PeerData localPeerData;
	public ArrayList<PeerData> neighbours;
	public ArrayList<PeerData> peerList;
	private WebServer rpcServer;
	private XmlRpcClient client;
	public int packet_sent, packet_received;
	
	private HashMap<PeerData, Integer> peerLatency;
	private HashMap<PeerData, Integer> peerDistance;
	public HashMap<String, String> resources;

	/**
	 * Command line execution optional. Class can be separated.
	 */
	public static void main(String[] argv) {
		
		String name = "P1", addr = "localhost";
		
		int port = 6661, capacity = 100;
		if(argv.length > 3) {
			name = argv[0];
			addr = argv[1];
			port = Integer.parseInt(argv[2]);
			capacity= Integer.parseInt(argv[3]);
		}
		Peer peer1 = new Peer(name, addr, port, capacity);
		peer1.startServer();
		peer1.commandLoop();
	}
	
	public Peer(String name, String address, int port, int capacity) {
		Vector<String> lpd = new Vector<String>();
		lpd.add(name);
		lpd.add(address);
		lpd.add("" + port);
		lpd.add("" + capacity);

		packet_sent = packet_received = 0;
		localPeerData = new PeerData(lpd);
		neighbours = new ArrayList<PeerData>();
		peerList = new ArrayList<PeerData>();
		peerLatency = new HashMap<PeerData, Integer>(100);
		peerDistance = new HashMap<PeerData, Integer>(100);
		resources = new HashMap<String, String>(100);
		resources.put("r1", "dummy value 1");
		resources.put("r2", "dummy value 1");
		resources.put("r3", "dummy value 1");
		resources.put(name, "MY NAME");
		
		// Put some random junk in here
		Random rand = new Random();
		int min = 0, max = 1000, i = 0;
		for(; i < 5; ++i) {
			int randomNum = rand.nextInt(max - min + 1) + min;
			resources.put(Integer.toString(randomNum), Integer.toString(randomNum));
		}
		
	}

	/**
	 * Start the XML-RPC webserver
	 */
	public void startServer() {
		try {
			System.out.println("Attempting to start XML-RPC Server...");
			rpcServer = new WebServer(localPeerData.port);
			rpcServer.addHandler("Peer",
					XmlRpcHandlerFactory.createHandlerFor(new RPC(this)));
			rpcServer.start();

			System.out.println("Started successfully.");
			System.out.println("Accepting requests.");
		} catch (Exception exception) {
			System.out.println(exception.getMessage());
			System.err.println("Server: " + exception);
		}
	}


	/**
	 * Initiate commandloop to call internal functions.
	 */
	private void commandLoop() {
		String command = "";
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

		while (command != "exit") {
			System.out.print("\n" + localPeerData.name + ": ");
			try {
				command = reader.readLine();
				if(command != null) {
					System.out.println(parseCommand(command));
				}

			} catch (IOException e) {
				System.out.println("Could not read input: " + e.getMessage());
				e.printStackTrace();
			}
		}
	}
	
	/**
	 * CLI parser, interpreting the input line and executing appropriate commands
	 */
	public String parseCommand(String cmd) {
		String args[] = cmd.split(" ");
		String initial = args[0];
		
		// Create an overlay network, optimize by closest port or highest capacity
		if(initial.equals("overlay")) {
			if(args.length < 2) {
				return "usage: overlay {ports, capacity}\n";
			}
			if(args[1].startsWith("p")) {
				return optimize_by_portnumbers();
			}
			if(args[1].startsWith("c")) {
				return optimize_by_capacity();
			}
		}
		
		// Status
		if(initial.startsWith("stat")) {
			String ret = localPeerData.name + ": Capa: " + localPeerData.capacity + ", Ns:" + neighbours.size();
			ret += ", Ps: " + peerList.size() + ", Rx: " + packet_received + ", Tx: " + packet_sent;
			return ret;
		}
		
		// Store resource
		if(initial.equals("store")) {
			if(args.length < 3) {
				return "usage: store KEY VALUE\n";
			}
			String key = args[1], value = args[2];
			resources.put(key, value);
			return "stored '" + key + "' succesfully\n";
		}
		
		// Find resource
		if(initial.equals("find")) {
			if(args.length < 2) {
				return "usage: find key\n";
			}
			String key = args[1];
			String ret = "";
			int ttl = 3;
			if(args.length == 3) {
				ttl = Integer.parseInt(args[2]);
			}
			if(resources.containsKey(key))
				ret = resources.get(key);
			else
				ret = findResource(key, ttl);
			if(ret.length() > 0)
				return "found resource '" + key + "' = '" + ret + "'";
			else 
				return "could not find resource: '" + key + "'\n";
		}
		
		// Walk-Find resource
		if(initial.equals("wfind")) {
			if(args.length < 2) {
				return "usage: find key\n";
			}
			String key = args[1];
			String ret = "";
			int ttl = 3;
			if(args.length == 3) {
				ttl = Integer.parseInt(args[2]);
			}
			if(resources.containsKey(key))
				ret = resources.get(key);
			else
				ret = wFindResource(key, ttl);
			if(ret.length() > 0)
				return "found resource '" + key + "' = '" + ret + "'";
			else 
				return "could not find resource: '" + key + "'\n";
		}
		
		// Peer discovery
		if(initial.equals("hello")) {
			query(args[1], Integer.parseInt(args[2]), 3);
			return localPeerData.name + ": Trying to query: " + args[1] + " at port " + args[2] + "...";
		} 
		// See the capacity
		if(initial.equals("capacity")) {
			return "Capacity: " + localPeerData.capacity + ", Overlay size: " + neighbours.size() + "\n";
		}
		// Change name
		if(initial.equals("name")) {
			String newname = args[1];
			localPeerData.name = newname;
			return "Changed name to: " + newname;
		}
		// Find Peer-Distance
		if(initial.equals("dist")) {
			String peer_dist = "";
			if(args.length > 1) {
				peer_dist = "dist(" + args[1] + ") = " + getPeerDistanceByName(args[1]);
			}
			else {
				peer_dist = listPeerDistance();
			}
			return "\n" + peer_dist;
		}
		// Get peer list 
		if(cmd.equals("plist")) {
			String plist = getPlist();
			return plist;
		}
		// Get neighbour list
		if(cmd.equals("nlist")) {
			return getNlist();
		}
		// Get neighbour list in .dot format
		if(cmd.equals("nlistdot")) {
			return getNlistDot();
		}
		// newer version
		if(cmd.equals("nlistdot2")) {
			return getNlistDot2();
		}
		// Get peer list in .dot format
		if(cmd.equals("plistdot")) {
			return getPlistDot();
		}
		
		// Request neighbourship
		if(initial.equals("greet")){
			if(args.length < 3 || args[1].equals("") || args[2].equals("")){
				return "Syntax error: greet address port";
			}
			for(PeerData pd : peerList){
				if(args[1].equals(pd.address) && args[2].equals(Integer.toString(pd.port))){
					greet(pd);
				}
			}
		}
		
		// Ping
		if(initial.equals("ping")) {
			String peer_name = args[1];
			int latency = -1;
			String addr = "";
			int port = -1;
			// if the arg is an address
			if(args.length > 2) {
				addr = args[1];
				port = Integer.parseInt(args[2]);
				peer_name = addr + " " + port;
			}
			// if the arg is a name
			else {
				for(PeerData pd : peerList) {
					if(pd.name.equals(peer_name)) {
						addr = pd.address;
						port = pd.port;
						break; // stop searching...
					}
				}
			}
			if(port != -1)
				latency = ping(addr, port);
			if(latency == -1)
				return "Peer '" + peer_name + "' seems to be offline..";
			else
				return "ping '" + peer_name + "', " + latency + "ms";
		}
		// get me out of here :0
		if(cmd.equals("exit")) {
			rpcServer.shutdown();
			return localPeerData.name + ": Exiting...";
		}
		return "Unrecognized command: '" + cmd + "', please try again";
	}
	
	
	/**
	 * Commands of the logic class here:
	 * *********************************
	 */

	
	private String findResource(String key, int ttl) {
		String ret = "no match";
		for(PeerData p : neighbours) {
			++this.packet_sent;
			try {
				System.out.println(localPeerData.name + ": Asking " + p.address + ":" + p.port + " for '" + key + "'");
				RPCApi remote_api = XmlRpc.createClient(RPCApi.class, "Peer", p.address, p.port);
				ret = remote_api.find(key, localPeerData.name, ttl);
				++packet_received; // increments cause we got an answer
				if(ret.length() > 0)
					return ret;
			} catch(Exception e) {
				e.printStackTrace();
			}
		}
		return ret;
	}

	private String wFindResource(String key, int ttl) {
		String ret = "no match";
		for(PeerData p : neighbours) {
			try {
				// just ask 20% of our neighbours (on pseudo-average)
				Random rand = new Random();
				int min = 0, max = 100;
				int randomNum = rand.nextInt(max - min + 1) + min;
				if(randomNum > 20)
					continue;
				++this.packet_sent;
				System.out.println(localPeerData.name + ": Asking " + p.address + ":" + p.port + " for '" + key + "'");
				RPCApi remote_api = XmlRpc.createClient(RPCApi.class, "Peer", p.address, p.port);
				ret = remote_api.walk(key, localPeerData.name, ttl);
				++packet_received; // increments cause we got an answer
				if(ret.length() > 0)
					return ret;
			} catch(Exception e) {
				e.printStackTrace();
			}
		}
		return ret;
	}

	void getPeerDistance() {
		for(PeerData pd : peerList) {
			int distance = getPeerDistanceByName(pd.name);
			peerDistance.put(pd, distance);
		}
	}
	
	String listPeerDistance() {
		String res = "";
		for(PeerData pd : peerList) {
			int dist = getPeerDistanceByName(pd.name);
			peerDistance.put(pd, dist);
			res += "dist(" + pd.name + ":" + pd.port + ") = " + dist + "\n";
		}
		return res;
	}

	int getPeerDistanceByName(String peername) {
		int distance = 2000;
		PeerData peer = null;
		for(PeerData p : peerList) {
			if(p.name.equals(peername)) {
				peer = p;
				break;
			}
		}
		if(peer != null) {
			// cartesian distance
			distance = peer.port - localPeerData.port;
			if(distance < 0)
				distance = distance * (-1);
		}
		return distance;
	}
	

	public int ping(String addr, int port) {
		int latency = 0;
		try {
			long systime = System.currentTimeMillis();
			RPCApi remote_api = XmlRpc.createClient(RPCApi.class, "Peer", addr, port);
			remote_api.ping();
			latency = (int) (System.currentTimeMillis() - systime);
		} catch(Exception e) {
			e.printStackTrace();
			latency = -1;
		}
		// Find Peer
		for(PeerData pd : peerList) {
			// add his latency to the list
			if(pd.address.equals(addr) && pd.port == port) {
				peerLatency.put(pd, latency);
			}
		}
		return latency;
	}

	public void query(String address, int port, int ttl) {
		ArrayList<PeerData> result = null;
		try {
			RPCApi remote_api = XmlRpc.createClient(RPCApi.class, "Peer",
					address, port);
			result = remote_api.query(ttl, localPeerData.name);

			// Add results
			for (PeerData p : result) {
				if (!peerList.contains(p) &&
					!p.equals(localPeerData) /* && 
					 peerList.size() < localPeerData.capacity
					 */
					) {
					System.out.println(localPeerData.name + ": Found peer:" + p.name);
					peerList.add(p);
				}
			}

		} catch (Exception exception) {
			System.err.println("Client: " + exception);
			System.out.println("");
		}
	}
	
	// Optimize the overlay network: find neighbours
	public String optimize_by_capacity() {
		String ret = "";
		int low_limit = localPeerData.capacity;
		ArrayList<PeerData> ignoreList = new ArrayList<PeerData>();

		while(neighbours.size() < low_limit) {
			PeerData target = null;
			int highest_capa = 0;
			// Try to peer with guys that have large capacity 
			for(PeerData pd : peerList) {
				if(!ignoreList.contains(pd) && pd.capacity > highest_capa) {
					target = pd;
					highest_capa = pd.capacity;
				}
			}
			if(target != null) {
				ret += "\nGreeting " + target.name;
				greet(target);
				ignoreList.add(target);
			} else {
				break;
			}
		}
		return ret;
	}
	
	// Optimize network by port-distance
	String optimize_by_portnumbers() {
		int low_limit = localPeerData.capacity;
		ArrayList<PeerData> ignoreList = new ArrayList<PeerData>();
		String ret = "";
		// check if we have room for more neighbours,
		int ncount = neighbours.size();
		while(neighbours.size() < low_limit) {
			int vacancy = localPeerData.capacity - ncount;
			if(vacancy > 0) {
				// .. if so, find the closest
				PeerData closest = null;
				int smallest = 100000;
				for(PeerData pd : peerList) {
					int dist = getPeerDistanceByName(pd.name);
					if(dist < smallest && !ignoreList.contains(pd)) {
						closest = pd;
						smallest = dist;
					}
					
				}
				// .. and try to greet him
				if(closest != null) {
					ret += greet(closest) + "\n";
					ignoreList.add(closest);
				}
				else {
					break;
				}
			} else {
				ret += "\nNo more room for peers in the overlay network\n";
				break;
			}
		}
		return ret;
	}

	public String getPlist() {
		String ret = "";
		ret += localPeerData.name + ": Peerlist:";
		for (PeerData pd : peerList) {
			int latency = -1;
			try {
				latency = peerLatency.get(pd);
			} catch(Exception e) {
				;
			}
			ret += "Name: " + pd.name + ", Address: " + pd.address
					+ ", Port: " + pd.port + " Capacity: " + pd.capacity 
					+ " Latency: " + latency + " Distance: " + peerDistance.get(pd) + "\n";
		}
		return ret;
	}
	
	public String getNlist() {
		String ret = "";
		for(PeerData pd : neighbours) {
			ret += pd.name + "(" + pd.address + ":" + pd.port + ", " + pd.capacity + ")\n";
		}
		return ret;
	}

	public String getNlistDot2() {
		String ret = "\t/* " + localPeerData.name + " */\n";
		int pnum = Integer.parseInt(localPeerData.name.substring(1));
		String prefix = ""; //"graph network {\n\toverlap = false;\n";
		String postfix = ""; //"}";
		/*
		for(PeerData pd : neighbours) {
			ret += "\t\"" + pd.name + "(" + pd.capacity + ")\";\n";
		}
		*/
		ret += "\tedge[color=" + colors[pnum] + "]\n";
		for(PeerData pd : neighbours) {
			int nnum = Integer.parseInt(pd.name.substring(1));
			if(nnum > pnum) {
				ret += "\t\"" + pd.name + "(" + pd.capacity + ")\" -- ";
				ret += "\"" + localPeerData.name + "(" + localPeerData.capacity + ")\";\n";
			}
		}
		return prefix + ret + postfix;
	}

	
	/* Returns String containing a .dot file graphing the neighbour network */ 
	public String getNlistDot() {
		String ret = "";
		String prefix = "graph network {\n\toverlap = false;\n";
		String postfix = "}";
		for(PeerData pd : neighbours) {
			ret += "\t\"" + pd.name + "(" + pd.capacity + ")\";\n";
		}
		for(PeerData pd : neighbours) {
			ret += "\t\"" + pd.name + "(" + pd.capacity + ")\" -- ";
			ret += "\"" + localPeerData.name + "(" + localPeerData.capacity + ")\";\n";
		}
		return prefix + ret + postfix;
	}
	/* Returns String containing a .dot file graphing the peer network */ 
	public String getPlistDot() {
		String ret = "";
		String prefix = "graph network {\n\toverlap = false;\n";
		String postfix = "}";
		for(PeerData pd : peerList) {
			ret += "\t\"" + pd.name + "(" + pd.capacity + ")\";\n";
		}
		for(PeerData pd : peerList) {
			ret += "\t\"" + pd.name + "(" + pd.capacity + ")\" -- ";
			ret += "\"" + localPeerData.name + "(" + localPeerData.capacity + ")\";\n";
		}
		return prefix + ret + postfix;
	}
	
	
	/**
	 * Try to greet a remote peer and request neighbourship.
	 * @param rPD
	 */
	public String greet(PeerData rPD){
		String ret = "";
		try {
			ret += "Requesting neighbourship with: "+rPD.address+":"+rPD.port + "\n";
			RPCApi remote_api = XmlRpc.createClient(RPCApi.class, "Peer",
					rPD.address, rPD.port);
			boolean answer = remote_api.greet(localPeerData);

			if(answer == true){
				ret += "Accepted. Now neighbour with "+rPD.name + "\n";
				neighbours.add(rPD);
			} else {
				ret += "Denied. Rejected by "+rPD.name + "\n";
			}
			
		} catch (Exception e) {
			ret += "Client: " + e + "\n";
		}
		return ret;
	}
}
